Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vPSBT: add sighash support (flattened virtual transactions) #759

Closed
wants to merge 2 commits into from

Conversation

GeorgeTsagk
Copy link
Member

@GeorgeTsagk GeorgeTsagk commented Jan 9, 2024

Description

This PR adds sighash support for vPSBTs

Todo list

  • Decide on the set of sighash flags to support for witnesses
  • Add sighash logic for validation
  • Add extra vPSBT tests that cover the supported sighashes
  • Add itest for trustless swap

Closes #577

@GeorgeTsagk GeorgeTsagk requested a review from guggero January 9, 2024 17:01
@GeorgeTsagk GeorgeTsagk self-assigned this Jan 9, 2024
@dstadulis dstadulis added this to the v0.4 milestone Jan 9, 2024
@Roasbeef Roasbeef self-requested a review January 9, 2024 20:10
Copy link
Member

@guggero guggero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice, I think you're on the right track!

Though I think we might need the new script version @Roasbeef mentioned yesterday and map out the inputs/outputs directly instead of as a 1-in-one-out. See inline comment.

tappsbt/interface.go Outdated Show resolved Hide resolved
tapscript/tx.go Outdated Show resolved Hide resolved
vm/vm.go Outdated
@@ -280,6 +280,20 @@ func (vm *Engine) validateStateTransition(virtualTx *wire.MsgTx) error {
}

for i, witness := range vm.newAsset.PrevWitnesses {
// TODO(george): Dummy assignment, need to figure out how to get
// the sighash byte from the signature.
sighashType := txscript.SigHashType(witness.TxWitness[0][33])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convention is: If the signature is exactly 64 bytes, it's an implied SigHashDefault. If it's 65 bytes, it is a different sighash and the 65th byte is the sighash byte.
We probably also need some logic to find out which entry of the witness is an actual signature.

Hmm... and that just get me thinking. Maybe we need a different way of transporting the sighash flag than by appending it to the witness. Because we look at the sighash on the level "vPSBT -> dummy TX". But the underlying Taproot execution VM will look at the sighash on the level "dummy TX -> txsighash", where we'll probably want to always use SigHashDefault. So the actual signature shouldn't contain the sighash as on the lowest level we always want it to be SigHashDefault. But when validating the asset, we need to know what the "vPSBT -> dummy TX" sighash was. So we need to transport it somewhere.

Or maybe this is an argument for actually mapping the vPSBT inputs/outputs directly into dummy TX inputs/outputs, then we can use the same sighash logic on both levels.

Copy link
Member Author

@GeorgeTsagk GeorgeTsagk Jan 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, thanks for writing this up, seems like there's loss of information by using the 1-1 dummy tx if we want to use more sighash flags.

One thing that I hadn't realized is that when the vPSBT is no longer around, a future verifier will have to rely on the ChallengeWitness field of Proof in order to check that a past transfer was valid. That ChallengeWitness currently is the single 1-1 dummy tx (which relied on tap always using "sighash all"). This means that the ephemeral dummy txs constructed by each vInput in order for the backend to sign will no longer be reconstructible by the verifier in order to check the signature.

As you mention, we can expand the ChallengeWitness and have multiple inputs and outputs. I believe we can align the inputs and outputs so that at input[i] / output[i] you'll find the roots of the trees into which the inputs and outputs were inserted, with respect to that vInput's sighash flag. So if on the vPSBT level we have 10 vInputs and 10 vOutputs, if the first (index 0) vInput wants to use a sighash that commits to a single input and all the outputs then on the dummy tx level we'd have input[0] be produced by inserting to an ms-smt just vInput[0] and output[0] would be produced by inserting all vOutputs in an ms-smt.

Can we go even further than that and do a "raw" mapping?

We can have each input in the dummy tx be produced by inserting into an ms-smt just the one vInput that has the same index. We can do exactly the same for the outputs, with each one being produced by an ms-smt that only contains the single vOutput on the same index. At that point can we just leverage the sighashes on the btc level? Nobody knows that the inputs/outputs are dummy, but the signatures can normally apply to them in any sighash fashion already available to us via the backend.

If the latter works I believe it's the preferable approach?
LMK what you think

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is an issue and is very likely just a misunderstanding around the ChallengeWitness.
That field is a special field that is only set in ownership proof files. An ownership proof is only created if you want/need to prove to someone that you do have the asset-level key to spend an asset. So it is just a signature showing you could spend the asset, without actually spending it (so it's not a valid transfer proof).

So the field is only set for the output of ProveAssetOwnership and can only be verified with VerifyAssetOwnership.
The 1-in-1 out mentioned there is actually on the vPSBT level (not the dummy TX level), so it won't be affected by any changes we make due to the sighash implementation. Also, for the asset ownership proof we'll likely always use SigHashDefault.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have each input in the dummy tx be produced by inserting into an ms-smt just the one vInput that has the same index. We can do exactly the same for the outputs, with each one being produced by an ms-smt that only contains the single vOutput on the same index. At that point can we just leverage the sighashes on the btc level? Nobody knows that the inputs/outputs are dummy, but the signatures can normally apply to them in any sighash fashion already available to us via the backend.

Yes, I think that is what we want. We'd do a raw mapping and then just use the BTC-VM level sighash functionality.

@@ -296,6 +322,14 @@ func (vm *Engine) validateStateTransition(virtualTx *wire.MsgTx) error {
if err != nil {
return err
}
case asset.ScriptV1:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main motivation here is that vm.ValidateWitnessV0 relies on constructing the virtual transaction with single input / single output. We can't break that now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above. We can't unconditionally make the new tree.

for _, txAsset := range assetOutputs {
// If we have any asset splits, then we'll indirectly commit to
// all of them through the SplitCommitmentRoot.
if txAsset.SplitCommitmentRoot != nil {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can prove problematic for some sighash types. As this output implicitly commits to all outputs, altering the desired effect of the sighash

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right. I think we won't be able to support the SIGHASH_SINGLE use case on the asset level. Because when you create the outputs, you need to know all outputs, otherwise you can't create a split commitment.
But looking at the example given in #577 it seems like we mostly want the SIGHASH_NONE functionality (at least on the output side). So that should still be achievable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right. I think we won't be able to support the SIGHASH_SINGLE use case on the asset level.

Agreed. Though I think it doesn't matter, as the root bitcoin signature is what actually binds everything. See my comment above. We don't need to enumerate the outputs as the split does that for us already.

@GeorgeTsagk GeorgeTsagk requested a review from guggero January 17, 2024 16:48
Copy link
Member

@guggero guggero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're on the right track! Nice work so far 💯 Left a couple of comments.

@@ -140,7 +140,7 @@ func virtualGenesisTxOut(newAsset *Asset) (*wire.TxOut, error) {
// MockGroupTxBuilder.
func virtualGenesisTx(newAsset *Asset) (*wire.MsgTx, error) {
var (
txIn *wire.TxIn
txIn []*wire.TxIn
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rename to txIns to not shadow the name in the loop below (for _, txIn := range txIn)?

@@ -31,7 +31,7 @@ func BuildGenesisTx(newAsset *asset.Asset) (*wire.MsgTx,

// Now, create the virtual transaction that represents this asset
// minting.
virtualTx, _, err := VirtualTx(newAsset, nil)
virtualTx, _, err := VirtualTx(newAsset, nil, []*asset.Asset{newAsset})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks a bit confusing, as if the inputs and outputs were the same... Maybe it would be a bit more clear if the first parameter would just be the []PreviousWitness slice, since it looks like only that is being used for the input part of the virtual TX?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah why do we need to pass in the asset twice? I don't see why we need to make any changes here. I think also shows the rationale re the design for the splits: the root split includes the commitment to them all, so you only need to pass around the root split for validation purposes.

@@ -648,9 +648,14 @@ func SignVirtualTransaction(vPkt *tappsbt.VPacket, signer Signer,
prevAssets[input.PrevID] = input.Asset()
}

assetOutputs := make([]*asset.Asset, len(outputs))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could make use of the fn.Map() function here. Doesn't really make it shorter but makes review a bit easier IMO (since creation of the slice with make is abstracted away).

for _, txAsset := range assetOutputs {
// If we have any asset splits, then we'll indirectly commit to
// all of them through the SplitCommitmentRoot.
if txAsset.SplitCommitmentRoot != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right. I think we won't be able to support the SIGHASH_SINGLE use case on the asset level. Because when you create the outputs, you need to know all outputs, otherwise you can't create a split commitment.
But looking at the example given in #577 it seems like we mostly want the SIGHASH_NONE functionality (at least on the output side). So that should still be achievable.

@dstadulis
Copy link
Collaborator

Summarizing out-of-band updates:

PR will continue to implement:
Spreading out the pseudo-transactions inputs/outputs s.t. they directly match those in the vPSBT level.
Even though split commitment is a thorn atm w.r.t allowing to commit to individual outputs, we can still proceed with sighashes that make sense with what we have (sighash_all, sighash_none, anyone can pay)

Status

  • Add support for sighash checks
    @GeorgeTsagk starting:
  • Add itest that follows vPSBT signing flow and checks sighash_all / sighash_none

@@ -159,7 +159,10 @@ func virtualGenesisTx(newAsset *Asset) (*wire.MsgTx, error) {
// With our single input and output mapped, we're ready to construct our
// virtual transaction.
virtualTx := wire.NewMsgTx(2)
virtualTx.AddTxIn(txIn)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this can't be done unconditionally. We need to make a new TAP VM version, then flatten the virtual transaction only for that version.

@@ -159,7 +159,10 @@ func virtualGenesisTx(newAsset *Asset) (*wire.MsgTx, error) {
// With our single input and output mapped, we're ready to construct our
// virtual transaction.
virtualTx := wire.NewMsgTx(2)
virtualTx.AddTxIn(txIn)

for _, txIn := range txIn {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, coming back to this, I think I'm still not totally convinced we need to do this. Input level conditional signing can happen on the Bitcoin layer, in that you use the normal sig hash flags there to bind your Bitcoin level signature to the presence of one or all inputs.

In TAP, we have a sort of output centric model: you validate the output (asset leaf TLV), and the witness data is also stored in the output. You can control the environment needed to validate that output based on the logical spend group. The set of inputs here can only ever be the set of inputs referenced in the witness data for the output. Each input needs a witness, and is effectively always consolidating to that single output (ignoring splits for a second).

With the framing above, in what case would we ever need to sign only some of the inputs in that spend group? Changing the set of inputs actually changes the output, so those need to be fully bound. We don't have the ability to have loose binding of the inputs.

If you consider splits, then that's an area that I think we can already have covered as the sighash flag can determine which of the split out puts are inserted into the split output tree. For all, do everything. For none, do nothing. You can make single work somewhat

With all that said, I think it makes sense to make this an option, if we have indeed found a flow that can't work w/o a flattened virtual txn. Why wouldn't we also flatten the tx out as well?

As mentioned above, this requires updates elsewhere.

txCopy.TxIn[zeroIndex].PreviousOutPoint.Index = idx
txCopy.TxIn[zeroIndex].Sequence = uint32(input.RelativeLockTime)
txCopy.TxIn[zeroIndex].Witness = witness
txCopy.TxIn[idx].PreviousOutPoint.Index = idx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if we're not doing the 1-in-1-out version, then we don't need to swap out information for each input. Instead, the virtual tx has all the inputs, and idx here is just passed to sign/verify.

@@ -100,7 +100,11 @@ func VirtualGenesisTxIn(newAsset *Asset) (*wire.TxIn, mssmt.Tree, error) {

prevOut := VirtualTxInPrevOut(treeRoot)

return wire.NewTxIn(prevOut, nil, nil), inputTree, nil
txIns := []*wire.TxIn{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to change. There's only ever a single virtual txin still.

@@ -31,7 +31,7 @@ func BuildGenesisTx(newAsset *asset.Asset) (*wire.MsgTx,

// Now, create the virtual transaction that represents this asset
// minting.
virtualTx, _, err := VirtualTx(newAsset, nil)
virtualTx, _, err := VirtualTx(newAsset, nil, []*asset.Asset{newAsset})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah why do we need to pass in the asset twice? I don't see why we need to make any changes here. I think also shows the rationale re the design for the splits: the root split includes the commitment to them all, so you only need to pass around the root split for validation purposes.

for _, txAsset := range assetOutputs {
// If we have any asset splits, then we'll indirectly commit to
// all of them through the SplitCommitmentRoot.
if txAsset.SplitCommitmentRoot != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right. I think we won't be able to support the SIGHASH_SINGLE use case on the asset level.

Agreed. Though I think it doesn't matter, as the root bitcoin signature is what actually binds everything. See my comment above. We don't need to enumerate the outputs as the split does that for us already.


// We add each output as a standard transaction output.
for _, txOut := range txOuts {
virtualTx.AddTxOut(txOut)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't match the mock version in the tests above.

assetOutputs := make([]*asset.Asset, 0, len(vm.splitAssets)+1)

// Add the root asset to the list of outputs.
// TODO(george): was the root asset placed 1st on the signer's side? is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's always first in the split commitment tree.

// based on the sighash flag of the signature that we acquired
// above.
virtualTx, _, err := tapscript.VirtualTx(
vm.newAsset, vm.prevAssets, assetOutputs,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should take the script version as input. We can't break all the old versions (but again, I don't think this level of changes are needed: output sighashes are all or nothing).

@@ -296,6 +322,14 @@ func (vm *Engine) validateStateTransition(virtualTx *wire.MsgTx) error {
if err != nil {
return err
}
case asset.ScriptV1:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above. We can't unconditionally make the new tree.

@GeorgeTsagk GeorgeTsagk changed the title vPSBT: add sighash support vPSBT: add sighash support (flattened virtual transactions) Jan 31, 2024
@GeorgeTsagk
Copy link
Member Author

Closing this, replaced by #779.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: ✅ Done
Development

Successfully merging this pull request may close these issues.

tapgarden/rpc: expose custom sighash flags for vPSBT funding to enable trustless asset swaps
4 participants